"""
@Author    : ghenyar
@Time      : 2025/8/28 13:00
@File      : files
@Desc      : 文件处理
"""
import os
import uuid
from pathlib import Path
from typing import List, Optional

from fastapi import UploadFile, Request
from starlette.responses import FileResponse, StreamingResponse
from urllib.parse import quote


class FileUtils:

    @staticmethod
    def upload_file(file, file_path):
        """
        文件上传
        :param file: 所属上传的文件
        :param file_path: 文件所属文件夹 如：avatar
        :return:
        """

        try:
            # 构造保存目录
            save_path = os.path.join(os.getcwd(), "static/uploads", file_path)
            # 不存在则创建目录
            os.makedirs(save_path, exist_ok=True)
            # 生成UUID文件名
            file_name = str(uuid.uuid4())
            # 获取文件扩展名
            extension = os.path.splitext(file.filename)[1]
            new_file_name = file_name + extension
            # 构造完整的保存路径
            full_save_path = os.path.join(save_path, new_file_name)
            # 以二进制写入模式打开文件
            with open(full_save_path, "wb") as f:
                # 写入文件内容
                f.write(file.file.read())
            return {"error": 0, "file_name": new_file_name, "original_name": file.filename,
                    "full_path": full_save_path, "message": "上传成功"}
        except Exception as e:
            return {"error": -1, "original_name": file.filename, "message": "上传失败、" + str(e)}

    @classmethod
    async def download(cls, file_name: str, request: Request):
        """
        二进制预览或下载文件
        :param file_name:
        :param request:
        :return:
        """
        # 查找指定文件
        files_path = await cls.__get_file_path(file_name)
        if files_path is None:
            return None
        return await cls.convert_to_binary(request, files_path)

    @classmethod
    async def delete_file(cls, file_name: str):
        """
        删除文件
        @param file_name:文件名
        @return:
        """
        files_path = await cls.__get_file_path(file_name)
        if os.path.exists(files_path) and Path(files_path).exists():
            os.remove(files_path)

    @classmethod
    async def segmentedUpload(cls, chunk: UploadFile, chunkIndex: int, fileName: str, fileUuid: str,
                              has_uuid: Optional[bool],
                              file_path: Optional[str]):
        """
        文件分段上传
        :param chunk: 块
        :param chunkIndex: 块索引
        :param fileName: 上传文件名称
        :param fileUuid: 前端生成的uuid
        :param has_uuid: 文件名是否改成uuid  True:是  False:否
        :param file_path: 上传问价所属类   如:头像(avatar),图片(img),pdf等等
        :return:
        """
        try:
            if not file_path:
                file_path = "file"
            save_path = os.path.join(os.getcwd(), "static/uploads", file_path)
            # 不存在则创建目录
            os.makedirs(save_path, exist_ok=True)
            new_file_name = fileName
            if has_uuid:
                # 获取文件扩展名
                extension = os.path.splitext(fileName)[1]
                new_file_name = fileUuid + extension
            # 构造完整的保存路径
            full_save_path = os.path.join(save_path, f"{new_file_name}.part{chunkIndex}")
            with open(full_save_path, "wb") as f:
                f.write(await chunk.read())
            return {"code": 200, "file_name": new_file_name, "original_name": fileName, "message": "上传成功"}
        except Exception as e:
            return {"code": 400, "message": f"上传失败: {str(e)}"}

    @classmethod
    async def segmented_merge_chunks(cls, chunk: dict):
        """
        合并分块上传的文件
        :param chunk: 分块
        :return:
        """
        file_list = await cls._list_file_names()
        directory_path = None
        for file_name in file_list:
            if Path(file_name).name.startswith(chunk['file_name']):
                directory_path = os.path.dirname(file_name)
                break
        result = await cls._merge_file_chunks(directory_path, chunk['file_name'], chunk['totalChunks'])
        return result, "{}/{}".format(directory_path, chunk['file_name'])

    @staticmethod
    async def _merge_file_chunks(dir_name: str, file_name: str, totalChunks: int):
        """
        合并文件块
        :param dir_name: 所在文件夹
        :param file_name: 文件名
        :param totalChunks: 总块数
        """
        final_file_path = os.path.join(dir_name, file_name)
        try:
            with open(final_file_path, "wb") as final_file:
                for i in range(totalChunks):
                    chunk_path = os.path.join(dir_name, f"{file_name}.part{i}")
                    with open(chunk_path, "rb") as chunk_file:
                        chunk_data = chunk_file.read()
                        final_file.write(chunk_data)
                    os.remove(chunk_path)
        except Exception as e:
            return str(e)
        return True

    @classmethod
    async def convert_to_binary(cls, request, files_path):
        # 转二进制
        file_extension = Path(files_path).suffix
        file_name = Path(files_path).name
        if file_extension == ".svg":
            return FileResponse(path=files_path, media_type="image/svg+xml", filename=file_name)
        elif file_extension == ".mp4" or file_extension == ".webm" or file_extension == ".avi":
            media_type = "video/{}".format(file_extension[1:])
            return await cls.__stream_file(request, files_path, file_name, media_type)
        elif file_extension == ".mp3" or file_extension == ".wav" or file_extension == ".aac" or file_extension == ".aiff" or file_extension == ".wma":
            media_type = "audio/mpeg".format(file_extension[1:])
            return await cls.__stream_file(request, files_path, file_name, media_type)
        elif file_extension == ".jpg" or file_extension == ".jpeg" or file_extension == ".png" or file_extension == ".gif":
            media_type = "image/{}".format(file_extension[1:])
            return await cls.__stream_file(request, files_path, file_name, media_type)
        else:
            return await cls.__stream_file(request, files_path, file_name)

    @staticmethod
    async def __stream_file(request: Request, file_path: str, file_name: str,
                            media_type: Optional[str] = "application/octet-stream"):
        """
        生成流式
        :param request: 请求体
        :param file_path: 文件路径
        :param file_name: 文件名称
        :param media_type: 文件类型
        :return:
        """
        range_header = request.headers.get('range')
        file_size = os.path.getsize(file_path)
        # 每次请求1MB
        chunk_size = 1024 * 1024 * 2  # 1MB
        if range_header is None:
            start, end = 0, chunk_size - 1
        else:
            start, end = range_header.replace("bytes=", "").split("-")
            start = int(start)
            end = int(end) if end else start + chunk_size - 1
        # 确保end不超过文件大小
        if end >= file_size:
            end = file_size - 1
        # 确保最后一个块小于1MB
        if end - start + 1 > chunk_size:
            end = start + chunk_size - 1

        async def binary(start_r: int, end_r: int):
            with open(file_path, mode="rb") as file_like:  # 以二进制模式打开文件
                file_like.seek(start_r)
                while start_r <= end_r:
                    bytes_to_read = min(1024 * 1024, end_r - start_r + 1)
                    data = file_like.read(bytes_to_read)
                    if not data:
                        break
                    start_r += len(data)
                    yield data

        headers = {
            "Content-Range": f"bytes {start}-{end}/{file_size}",
            "Accept-Ranges": "bytes",
            "Content-Length": str(end - start + 1),
            "Content-Disposition": f"attachment; filename={quote(file_name)}",
            "Content-Type": media_type
        }
        return StreamingResponse(binary(start, end), headers=headers, status_code=206)

    @classmethod
    async def __get_file_path(cls, file_name: str):
        """
        根据文件名获取文件
        :param file_name: 文件名
        :return:
        """
        file_list = await cls._list_file_names()
        _, file_extension = os.path.splitext(file_name)
        if not file_extension:
            return next((name for name in file_list if Path(name).stem == file_name), None)
        else:
            for files in file_list:
                if Path(files).name == file_name:
                    return files
        return None

    @staticmethod
    async def _list_file_names() -> List[str]:
        """
        列出目录下的文件，排除.DS_Store和.conf文件
        :return:
        """
        return [str(p) for p in Path(os.path.join(os.getcwd(), "static/uploads")).glob("**/*") if
                p.is_file() and not p.name.endswith((".DS_Store", ".conf"))]
